package com.lxw.multidb.modules.sql;

import com.lxw.multidb.controller.dto.ColumnInfo;
import com.lxw.multidb.controller.dto.DbConnectionRequest;
import com.lxw.multidb.controller.dto.FieldMappingDTO;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.stereotype.Service;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 数据库元数据服务类
 * 提供多数据库（MySQL、SQL Server、PostgreSQL、KingBase）的连接管理和元数据查询功能
 * <p>
 * 主要功能：
 * 1. 数据源连接池管理（使用HikariCP）- 即用即创建，用完即关闭
 * 2. 数据库列表查询
 * 3. 表列表查询
 * 4. 字段列表查询
 * 5. 表数据查询（支持字段映射、条件过滤和分页）
 * <p>
 * 资源管理策略：
 * - 不使用缓存，每次请求都创建新的数据源
 * - 使用try-with-resources确保数据源自动关闭，避免资源泄漏
 * - 连接池配置较小（最大5个连接），适合临时查询场景
 *
 * @author lxw
 * @version 1.2
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class DatabaseMetaService {

    /**
     * 创建数据源（连接池）
     * 每次调用都会创建新的HikariCP数据源实例，不使用缓存
     * 使用完毕后必须调用close()方法关闭数据源，建议使用try-with-resources自动管理
     * <p>
     * 连接池配置：
     * - 指定数据库名：标准连接池（最大5个连接，最小2个空闲）
     * - 未指定数据库名：小型连接池（最大2个连接，最小1个空闲）
     *
     * @param req 数据库连接请求对象
     * @return HikariCP数据源实例，已通过连接测试
     * @throws RuntimeException 当数据源创建或连接测试失败时抛出异常
     */
    public HikariDataSource createDataSource(DbConnectionRequest req) {
        if (StringUtils.isBlank(req.getDatabaseName())) {
            log.info("创建临时数据源（用于查询数据库列表），数据库类型: {}, 主机: {}:{}",
                    req.getDbType(), req.getHost(), req.getPort());
            return buildDataSource(req, 2, 1);
        }

        log.info("创建数据源，数据库类型: {}, 主机: {}:{}, 数据库: {}",
                req.getDbType(), req.getHost(), req.getPort(), req.getDatabaseName());
        return buildDataSource(req, 5, 2);
    }

    /**
     * 查询数据库列表
     *
     * @param req 数据库连接请求对象（不需要指定databaseName）
     * @return 数据库名称列表
     * @throws RuntimeException 当查询失败时抛出异常
     */
    public List<String> listDatabases(DbConnectionRequest req) {
        List<String> databaseList = new ArrayList<>();

        try (HikariDataSource dataSource = createDataSource(req);
             Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            String sql;
            // 根据不同数据库类型执行相应的查询语句
            switch (req.getDbType().toLowerCase()) {
                case "mysql" -> sql = "SHOW DATABASES";
                case "sqlserver" ->
                        sql = "SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb')";
                case "postgresql" ->
                        sql = "SELECT datname FROM pg_database WHERE datistemplate = false AND datname NOT IN ('postgres')";
                case "kingbase" ->
                        sql = "SELECT datname FROM pg_database WHERE datistemplate = false AND datname NOT IN ('template0', 'template1')";
                default -> throw new RuntimeException("不支持的数据库类型：" + req.getDbType());
            }

            try (ResultSet rs = stmt.executeQuery(sql)) {
                while (rs.next()) {
                    databaseList.add(rs.getString(1));
                }
            }

            log.info("成功查询到 {} 个数据库", databaseList.size());

        } catch (SQLException e) {
            log.error("查询数据库列表失败，数据库类型: {}, 主机: {}:{}",
                    req.getDbType(), req.getHost(), req.getPort(), e);
            throw new RuntimeException("查询数据库列表失败: " + e.getMessage(), e);
        }

        return databaseList;
    }

    /**
     * 查询指定数据库中的表列表
     * 使用JDBC元数据API获取表信息，只返回TABLE类型
     *
     * @param req 数据库连接请求对象（需要指定databaseName）
     * @return 表名称列表
     * @throws RuntimeException 当查询失败时抛出异常
     */
    public List<String> listTables(DbConnectionRequest req) {
        List<String> tableList = new ArrayList<>();

        try (HikariDataSource dataSource = createDataSource(req);
             Connection conn = dataSource.getConnection()) {

            Pair<String, String> catalogSchema = getCatalogAndSchema(req.getDbType(), req.getDatabaseName());
            DatabaseMetaData metaData = conn.getMetaData();

            try (ResultSet tables = metaData.getTables(
                    catalogSchema.getLeft(),
                    catalogSchema.getRight(),
                    "%",
                    new String[]{"TABLE"}
            )) {
                while (tables.next()) {
                    tableList.add(tables.getString("TABLE_NAME"));
                }
            }

            log.info("成功查询到 {} 张表，数据库: {}", tableList.size(), req.getDatabaseName());

        } catch (SQLException e) {
            log.error("查询表列表失败，数据库: {}", req.getDatabaseName(), e);
            throw new RuntimeException("查询表列表失败: " + e.getMessage(), e);
        }

        return tableList;
    }

    /**
     * 查询指定表的字段列表
     * 使用JDBC元数据API获取列信息
     *
     * @param req 数据库连接请求对象（需要指定databaseName和tableName）
     * @return 字段名称列表
     * @throws RuntimeException 当查询失败时抛出异常
     */
    public List<String> listColumns(DbConnectionRequest req) {
        List<String> columnList = new ArrayList<>();

        if (req.getTableName() == null || req.getTableName().trim().isEmpty()) {
            throw new IllegalArgumentException("表名不能为空");
        }

        try (HikariDataSource dataSource = createDataSource(req);
             Connection conn = dataSource.getConnection()) {

            Pair<String, String> catalogSchema = getCatalogAndSchema(req.getDbType(), req.getDatabaseName());
            DatabaseMetaData metaData = conn.getMetaData();

            try (ResultSet columns = metaData.getColumns(
                    catalogSchema.getLeft(),
                    catalogSchema.getRight(),
                    req.getTableName(),
                    "%"
            )) {
                while (columns.next()) {
                    columnList.add(columns.getString("COLUMN_NAME"));
                }
            }

            log.info("成功查询到 {} 个字段，表: {}", columnList.size(), req.getTableName());

        } catch (SQLException e) {
            log.error("查询字段列表失败，表: {}", req.getTableName(), e);
            throw new RuntimeException("查询字段列表失败: " + e.getMessage(), e);
        }

        return columnList;
    }

    // java
    public List<ColumnInfo> listColumnsWithMeta(DbConnectionRequest req) {
        List<ColumnInfo> columnList = new ArrayList<>();

        try (HikariDataSource dataSource = createDataSource(req);
             Connection conn = dataSource.getConnection()) {

            Pair<String, String> catalogSchema = getCatalogAndSchema(req.getDbType(), req.getDatabaseName());
            DatabaseMetaData metaData = conn.getMetaData();

            try (ResultSet rs = metaData.getColumns(
                    catalogSchema.getLeft(),
                    catalogSchema.getRight(),
                    req.getTableName(),
                    "%")) {

                while (rs.next()) {
                    ColumnInfo ci = new ColumnInfo();
                    ci.setName(rs.getString("COLUMN_NAME"));
                    ci.setDataType(rs.getInt("DATA_TYPE"));       // java.sql.Types
                    ci.setTypeName(rs.getString("TYPE_NAME"));    // DB specific type name
                    ci.setColumnSize(rs.getInt("COLUMN_SIZE"));
                    ci.setDecimalDigits(rs.getInt("DECIMAL_DIGITS"));
                    ci.setNullable("YES".equalsIgnoreCase(rs.getString("IS_NULLABLE")));
                    ci.setRemarks(rs.getString("REMARKS"));       // 备注，某些驱动可能为空
                    columnList.add(ci);
                }
            }

            return columnList;

        } catch (SQLException e) {
            log.error("查询字段列表失败，表: {}", req.getTableName(), e);
            throw new RuntimeException("查询字段列表失败: " + e.getMessage(), e);
        }
    }

    /**
     * 查询表数据，支持字段映射、条件过滤和分页
     * <p>
     * 功能说明：
     * 1. 字段映射：可以将数据库字段名映射为更友好的显示名称
     * 2. 条件过滤：支持WHERE条件（如：age > 18 AND status = 'active'）
     * 3. 分页查询：支持指定页码和每页大小
     * <p>
     * 返回格式：
     * List<Map<String, Object>>，Map的key为显示字段名（经过映射后的），value为字段值
     * <p>
     * 注意：whereCondition参数会直接拼接到SQL中，调用方需要确保其安全性
     *
     * @param req 数据库连接请求对象（需要指定databaseName和tableName）
     * @return 查询结果列表
     * @throws RuntimeException 当查询失败时抛出异常
     */
    public List<Map<String, Object>> queryTableData(DbConnectionRequest req) {
        List<Map<String, Object>> resultList = new ArrayList<>();

        if (req.getTableName() == null || req.getTableName().trim().isEmpty()) {
            throw new IllegalArgumentException("表名不能为空");
        }

        try (HikariDataSource dataSource = createDataSource(req);
             Connection conn = dataSource.getConnection()) {

            String sql = buildQuerySql(req);
            log.debug("执行查询SQL: {}", sql);

            try (Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery(sql)) {

                ResultSetMetaData metaData = rs.getMetaData();
                int columnCount = metaData.getColumnCount();

                while (rs.next()) {
                    Map<String, Object> row = new LinkedHashMap<>();
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = metaData.getColumnLabel(i);
                        Object columnValue = rs.getObject(i);
                        row.put(columnName, columnValue);
                    }
                    resultList.add(row);
                }
            }

            log.info("成功查询到 {} 条数据，表: {}", resultList.size(), req.getTableName());

        } catch (SQLException e) {
            log.error("查询表数据失败，表: {}", req.getTableName(), e);
            throw new RuntimeException("查询表数据失败: " + e.getMessage(), e);
        }

        return resultList;
    }

    /**
     * 构建查询SQL语句
     * <p>
     * SQL结构：SELECT [字段列表] FROM [表名] [WHERE条件] [LIMIT/OFFSET分页]
     * <p>
     * 字段映射处理：
     * - 如果提供了fieldMappings，使用映射的字段和别名（如：db_field AS displayName）
     * - 如果未提供fieldMappings，查询所有字段（SELECT *）
     * <p>
     * 分页处理（不同数据库语法不同）：
     * - MySQL/PostgreSQL/KingBase: LIMIT pageSize OFFSET offset
     * - SQL Server: OFFSET offset ROWS FETCH NEXT pageSize ROWS ONLY
     *
     * @param req 数据库连接请求对象
     * @return 完整的SQL查询语句
     */
    private String buildQuerySql(DbConnectionRequest req) {
        StringBuilder sql = new StringBuilder("SELECT ");
        List<FieldMappingDTO> fieldMappings = req.getFieldMappings();

        // 构建字段列表
        if (fieldMappings != null && !fieldMappings.isEmpty()) {
            for (int i = 0; i < fieldMappings.size(); i++) {
                FieldMappingDTO mapping = fieldMappings.get(i);
                sql.append(mapping.getSourceField());

                // 如果指定了显示名称且不同于数据库字段名，添加别名
                if (StringUtils.isNotBlank(mapping.getTgtField())
                        && !mapping.getSourceField().equals(mapping.getTgtField())) {
                    sql.append(" AS ").append(mapping.getTgtField());
                }

                if (i < fieldMappings.size() - 1) {
                    sql.append(", ");
                }
            }
        } else {
            sql.append("*");
        }

        // 添加表名
        sql.append(" FROM ").append(req.getTableName());

        // 添加WHERE条件
        String whereCondition = req.getCondition();
        if (StringUtils.isNotBlank(whereCondition)) {
            sql.append(" WHERE ").append(whereCondition);
        }

        // 添加分页逻辑
        Integer pageIndex = req.getPageIndex();
        Integer pageSize = req.getPageSize();
        if (pageIndex != null && pageSize != null && pageIndex > 0 && pageSize > 0) {
            int offset = (pageIndex - 1) * pageSize;

            switch (req.getDbType().toLowerCase()) {
                case "mysql", "postgresql", "kingbase" -> {
                    sql.append(" LIMIT ").append(pageSize).append(" OFFSET ").append(offset);
                }
                case "sqlserver" -> {
                    // SQL Server的OFFSET必须配合ORDER BY使用
                    if (!sql.toString().toUpperCase().contains("ORDER BY")) {
                        sql.append(" ORDER BY (").append(req.getSortField()).append(") ");
                    }
                    sql.append(" OFFSET ").append(offset).append(" ROWS")
                            .append(" FETCH NEXT ").append(pageSize).append(" ROWS ONLY");
                }
                default -> throw new RuntimeException("不支持的数据库类型：" + req.getDbType());
            }
        }

        return sql.toString();
    }

    /**
     * 构建JDBC连接URL
     * <p>
     * 不同数据库的URL格式：
     * - MySQL: jdbc:mysql://host:port/database?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
     * - SQL Server: jdbc:sqlserver://host:port;databaseName=database;encrypt=false
     * - PostgreSQL: jdbc:postgresql://host:port/database
     * - KingBase: jdbc:kingbase8://host:port/database
     *
     * @param req 数据库连接请求对象
     * @return JDBC连接URL
     */
    private String buildJdbcUrl(DbConnectionRequest req) {
        return switch (req.getDbType().toLowerCase()) {
            case "mysql" -> {
                String url = "jdbc:mysql://" + req.getHost() + ":" + req.getPort();
                if (StringUtils.isNotBlank(req.getDatabaseName())) {
                    url += "/" + req.getDatabaseName();
                }
                url += "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
                yield url;
            }
            case "sqlserver" -> {
                String url = "jdbc:sqlserver://" + req.getHost() + ":" + req.getPort();
                if (StringUtils.isNotBlank(req.getDatabaseName())) {
                    url += ";databaseName=" + req.getDatabaseName();
                }
                url += ";encrypt=false";
                yield url;
            }
            case "postgresql" -> {
                /// PostgreSQL 驱动要求必须有数据库名，查询数据库列表时连接到默认的 postgres 数据库
                String url = "jdbc:postgresql://" + req.getHost() + ":" + req.getPort();
                if (StringUtils.isNotBlank(req.getDatabaseName())) {
                    url += "/" + req.getDatabaseName();
                } else {
                    url += "/postgres";
                }
                yield url;
            }
            case "kingbase" -> {
                // 人大金仓基于 PostgreSQL，同样要求必须有数据库名
                String url = "jdbc:kingbase8://" + req.getHost() + ":" + req.getPort();
                if (StringUtils.isNotBlank(req.getDatabaseName())) {
                    url += "/" + req.getDatabaseName();
                } else {
                    url += "/test";
                }
                yield url;
            }
            default -> throw new RuntimeException("不支持的数据库类型：" + req.getDbType());
        };
    }

    /**
     * 获取JDBC驱动类名
     *
     * @param dbType 数据库类型
     * @return JDBC驱动类的完全限定名
     */
    private String getDriverClassName(String dbType) {
        return switch (dbType.toLowerCase()) {
            case "mysql" -> "com.mysql.cj.jdbc.Driver";
            case "sqlserver" -> "com.microsoft.sqlserver.jdbc.SQLServerDriver";
            case "postgresql" -> "org.postgresql.Driver";
            case "kingbase" -> "com.kingbase8.Driver";
            default -> throw new RuntimeException("不支持的数据库类型：" + dbType);
        };
    }

    /**
     * 构建HikariCP数据源
     * <p>
     * 创建流程：
     * 1. 配置HikariCP连接池参数
     * 2. 创建HikariDataSource实例
     * 3. 测试数据库连接是否可用
     * 4. 如果测试失败，关闭数据源并抛出异常
     *
     * @param req         数据库连接请求对象
     * @param maxPoolSize 最大连接池大小
     * @param minIdle     最小空闲连接数
     * @return 配置完成且已通过连接测试的HikariDataSource实例
     * @throws RuntimeException 当数据源创建失败时抛出异常
     */
    private HikariDataSource buildDataSource(DbConnectionRequest req, int maxPoolSize, int minIdle) {
        HikariDataSource dataSource = null;

        try {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(buildJdbcUrl(req));
            config.setUsername(req.getUsername());
            config.setPassword(req.getPassword());
            config.setDriverClassName(getDriverClassName(req.getDbType()));
            config.setMaximumPoolSize(maxPoolSize);
            config.setMinimumIdle(minIdle);
            config.setConnectionTimeout(30000);
            config.setIdleTimeout(600000);
            config.setMaxLifetime(1800000);
            config.setPoolName("HikariPool-" + req.getDbType() + "-" +
                    (StringUtils.isNotBlank(req.getDatabaseName()) ? req.getDatabaseName() : "temp"));

            dataSource = new HikariDataSource(config);
            testConnection(dataSource, req);

            log.info("成功创建并测试数据源，数据库类型: {}, 主机: {}, 端口: {}, 数据库: {}, 连接池大小: {}/{}",
                    req.getDbType(), req.getHost(), req.getPort(), req.getDatabaseName(), minIdle, maxPoolSize);

            return dataSource;

        } catch (Exception e) {
            if (dataSource != null) {
                try {
                    dataSource.close();
                    log.info("创建数据源失败，已关闭并清理数据源资源");
                } catch (Exception closeException) {
                    log.error("关闭失败的数据源时发生异常", closeException);
                }
            }

            log.error("创建数据源失败，数据库类型: {}, 主机: {}, 端口: {}, 数据库: {}, 用户名: {}",
                    req.getDbType(), req.getHost(), req.getPort(), req.getDatabaseName(), req.getUsername(), e);

            throw new RuntimeException("数据源创建失败: " + e.getMessage(), e);
        }
    }

    /**
     * 测试数据库连接是否可用
     * 通过执行"SELECT 1"来验证数据源是否正常工作
     *
     * @param dataSource 要测试的数据源
     * @param req        数据库连接请求对象
     * @throws RuntimeException 当连接测试失败时抛出异常
     */
    private void testConnection(DataSource dataSource, DbConnectionRequest req) {
        try (Connection testConn = dataSource.getConnection();
             Statement testStmt = testConn.createStatement();
             ResultSet rs = testStmt.executeQuery("SELECT 1")) {

            if (rs.next()) {
                log.debug("数据库连接测试成功，数据库类型: {}, 主机: {}:{}",
                        req.getDbType(), req.getHost(), req.getPort());
            } else {
                throw new SQLException("连接测试查询未返回结果");
            }

        } catch (SQLException e) {
            log.error("数据库连接测试失败，数据库类型: {}, 主机: {}:{}, 数据库: {}",
                    req.getDbType(), req.getHost(), req.getPort(), req.getDatabaseName(), e);
            throw new RuntimeException("数据库连接测试失败，请检查连接配置: " + e.getMessage(), e);
        }
    }

    /**
     * 获取catalog和schema配置
     * <p>
     * 不同数据库对catalog和schema的使用：
     * - MySQL: 使用catalog表示数据库名
     * - PostgreSQL: 使用schema（默认为public）
     * - SQL Server: 同时使用catalog和schema
     * - KingBase: 同时使用catalog和schema
     *
     * @param dbType       数据库类型
     * @param databaseName 数据库名称
     * @return Pair对象，左侧为catalog，右侧为schema
     */
    private Pair<String, String> getCatalogAndSchema(String dbType, String databaseName) {
        String catalog = null;
        String schema = null;

        switch (dbType.toLowerCase()) {
            case "mysql" -> catalog = databaseName;
            case "postgresql" -> schema = "public";
            case "sqlserver" -> {
                catalog = databaseName;
                schema = "dbo";
            }
            case "kingbase" -> {
                catalog = databaseName;
                schema = "public";
            }
            default -> throw new RuntimeException("不支持的数据库类型：" + dbType);
        }

        return Pair.of(catalog, schema);
    }
}